<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGL2 - GPGPU - Closest Line</title>
<link type="text/css" href="resources/webgl-tutorials.css" rel="stylesheet" />
</head>
<body>
<div class="description">
  Closest Line
</div>
</body>
<!--
for most samples webgl-utils only provides shader compiling/linking and
canvas resizing because why clutter the examples with code that's the same in every sample.
See https://webgl2fundamentals.org/webgl/lessons/webgl-boilerplate.html
and https://webgl2fundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
for webgl-utils, m3, m4, and webgl-lessons-ui.
-->
<script src="resources/twgl-full.min.js"></script>
<script src="resources/m4.js"></script>

<script>
'use strict';

/* eslint no-alert: 0 */

function main() {
  const closestLineVS = `#version 300 es
  in vec4 position;
  void main() {
    gl_Position = position;
  }
  `;

  const closestLineFS = `#version 300 es
precision highp float;

uniform sampler2D pointsTex;
uniform sampler2D linesTex;
uniform int numLineSegments;

out int outColor;

vec4 getAs1D(sampler2D tex, ivec2 dimensions, int index) {
  int y = index / dimensions.x;
  int x = index % dimensions.x;
  return texelFetch(tex, ivec2(x, y), 0);
}

// from https://stackoverflow.com/a/6853926/128511
// a is the point, b,c is the line segment
float distanceFromPointToLine(in vec3 a, in vec3 b, in vec3 c) {
  vec3 ba = a - b;
  vec3 bc = c - b;
  float d = dot(ba, bc);
  float len = length(bc);
  float param = 0.0;
  if (len != 0.0) {
    param = clamp(d / (len * len), 0.0, 1.0);
  }
  vec3 r = b + bc * param;
  return distance(a, r);
}

void main() {
  ivec2 pointsTexDimensions = textureSize(pointsTex, 0);
  ivec2 linesTexDimensions = textureSize(linesTex, 0);

  int ndx = int(gl_FragCoord.y) * pointsTexDimensions.x + int(gl_FragCoord.x); 
  
  // find the closest line segment
  float minDist = 10000000.0; 
  int minIndex = -1;
  vec3 point = getAs1D(pointsTex, pointsTexDimensions, ndx).xyz;
  for (int i = 0; i < numLineSegments; ++i) {
    vec3 lineStart = getAs1D(linesTex, linesTexDimensions, i * 2).xyz;
    vec3 lineEnd = getAs1D(linesTex, linesTexDimensions, i * 2 + 1).xyz;
    float dist = distanceFromPointToLine(point, lineStart, lineEnd);
    if (dist < minDist) {
      minDist = dist;
      minIndex = i;
    }
  }
  
  outColor = minIndex;
}
`;

  // Get A WebGL context
  /** @type {HTMLCanvasElement} */
  const canvas = document.createElement("canvas");
  const gl = canvas.getContext("webgl2");
  if (!gl) {
    return;
  }

  const points = [
    100, 100,
    200, 100,
  ];
  const lines = [
     25,  50,
     25, 150,
     90,  50,
     90, 150,
    125,  50,
    125, 150,
    185,  50,
    185, 150,
    225,  50,
    225, 150,
  ];
  const numPoints = points.length / 2;
  const numLineSegments = lines.length / 2 / 2;

  const {tex: pointsTex, dimensions: pointsTexDimensions} =
      createDataTexture(gl, points, 2, gl.RG32F, gl.RG, gl.FLOAT);
  const {tex: linesTex, dimensions: linesTexDimensions} =
      createDataTexture(gl, lines, 2, gl.RG32F, gl.RG, gl.FLOAT);

  function createDataTexture(gl, data, numComponents, internalFormat, format, type) {
    const numElements = data.length / numComponents;

    // compute a size that will hold all of our data
    const width = Math.ceil(Math.sqrt(numElements));
    const height = Math.ceil(numElements / width);

    const bin = type === gl.FLOAT
        ? new Float32Array(width * height * numComponents)
        : new Int32Array(width * height * numComponents);
    bin.fill(type === gl.FLOAT ? Number.POSITIVE_INFINITY : 0);
    bin.set(data);

    const tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.texImage2D(
        gl.TEXTURE_2D,
        0,        // mip level
        internalFormat,
        width,
        height,
        0,        // border
        format,
        type,
        bin,
    );
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    return {tex, dimensions: [width, height]};
  }

  const closestLinePrgInfo = twgl.createProgramInfo(
      gl, [closestLineVS, closestLineFS]);

  // create a texture for the results
  const {tex: closestLinesTex, dimensions: closestLinesTexDimensions} =
      createDataTexture(gl, new Array(numPoints), 1, gl.R32I, gl.RED_INTEGER, gl.INT);

  function createFramebuffer(gl, tex) {
    const fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
    return fb;
  }

  // create a framebuffer so we can write to the closestLinesTex
  const closestLineFB = createFramebuffer(gl, closestLinesTex);

  // setup a full canvas clip space quad
  const quadBufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
        -1, -1,
         1, -1,
        -1,  1,
        -1,  1,
         1, -1,
         1,  1,
      ],
    },
  });
  const vao = twgl.createVAOFromBufferInfo(gl, closestLinePrgInfo, quadBufferInfo);

  // compute the closest lines
  gl.bindFramebuffer(gl.FRAMEBUFFER, closestLineFB);
  gl.viewport(0, 0, ...closestLinesTexDimensions);

  gl.bindVertexArray(vao);  // quad vertices
  gl.useProgram(closestLinePrgInfo.program);
  twgl.setUniforms(closestLinePrgInfo, {
    pointsTex,
    linesTex,
    numLineSegments,
  });
  gl.drawArrays(gl.TRIANGLES, 0, 6);  // draw the clip space quad so we get one result for each pixel

  // get the results.
  {
    const [width, height] = closestLinesTexDimensions;
    const results = new Int32Array(width * height * 4);
    gl.readPixels(0, 0, width, height, gl.RGBA_INTEGER, gl.INT, results);

    for (let i = 0; i < numPoints; ++i) {
      log('result:', results[i * 4]);
    }
  }
}

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}

main();
</script>
</html>
